use crate::limits::{total_limit, LimitError};
use crate::scalar_fn::{EvalScalar, ScalarFn};

pub fn derivative<T>(func: &T, limit_point: f64) -> Result<f64, LimitError>
where
    T: EvalScalar,
{
    let new_derivative = DerivativeScalarFns::new(func, limit_point);
    total_limit(limit_point, &new_derivative)
}

pub trait DerivativeScalar: EvalScalar {
    fn derivative(&self, limit_point: f64) -> f64 {
        let limit = self.limit(limit_point);
        limit.unwrap_or(0.0f64)
    }
}

#[derive(Clone, Debug)]
pub struct DerivativeScalarFns<'a, T>
where
    T: EvalScalar,
{
    func: &'a T,
    limit_point: f64,
}

impl<'a, T> DerivativeScalarFns<'a, T>
where
    T: EvalScalar,
{
    pub fn new(func: &'a T, limit_point: f64) -> Self {
        Self { func, limit_point }
    }
}

impl<'a, T> EvalScalar for DerivativeScalarFns<'a, T>
where
    T: EvalScalar,
{
    fn eval(&self, x: f64) -> f64 {
        let numerator = (self.func).eval(x) - (self.func).eval(self.limit_point);
        let denominator = x - self.limit_point;
        numerator / denominator
    }

    fn limit(&self, x: f64) -> Result<f64, LimitError> {
        total_limit(x, self)
    }

    fn get_fn(&self) -> Option<fn(f64, &Option<Vec<f64>>) -> f64> {
        self.func.get_fn()
    }

    fn get_params(&self) -> Option<Vec<f64>> {
        self.func.get_params()
    }
}

impl<'a, T> DerivativeScalar for DerivativeScalarFns<'a, T> where T: EvalScalar {}

pub fn compare_derivatives(x: f64, func1: &ScalarFn, func2: &ScalarFn) -> Result<f64, LimitError> {
    let f1_lim = derivative(func1, x)?;
    let f2_lim = derivative(func2, x)?;
    match f2_lim.abs() > f64::EPSILON {
        true => Ok(f1_lim / f2_lim),
        false => Err(LimitError::NaN),
    }
}

#[derive(Clone, Debug)]
pub struct OpOnDerivativeScalarFns<'a> {
    func1: &'a ScalarFn,
    func2: &'a ScalarFn,
    op: DerivativeOps,
    alpha: f64,
}

impl<'a> OpOnDerivativeScalarFns<'a> {
    pub fn new(
        func1: &'a ScalarFn,
        func2: &'a ScalarFn,
        op: DerivativeOps,
        param: Option<f64>,
    ) -> Self {
        let alpha = param.unwrap_or(1.0f64);
        OpOnDerivativeScalarFns {
            func1,
            func2,
            op,
            alpha,
        }
    }

    fn outer_limit(&self, limit_point: f64) -> Result<f64, LimitError> {
        derivative(self, limit_point)
    }

    fn inner_limit(&self, limit_point: f64) -> Result<f64, LimitError> {
        match self.op {
            DerivativeOps::Chain => {
                let f1 = self.func1.eval(limit_point);
                let ddf1 = derivative(self.func2, f1)?;
                let ddx1 = derivative(self.func1, limit_point)?;
                Ok(ddf1 * ddx1)
            }
            DerivativeOps::Pow => {
                let f1 = self.func1.eval(limit_point);
                let ddf1 = self.alpha * f1.powf(self.alpha - 1.0);
                let ddx1 = derivative(self.func1, limit_point)?;
                Ok(ddf1 * ddx1)
            }
            DerivativeOps::ProdRule => {
                let f1 = self.func1.eval(limit_point);
                let f2 = self.func2.eval(limit_point);
                let df1 = derivative(self.func1, limit_point)?;
                let df2 = derivative(self.func2, limit_point)?;
                Ok(f2 * df1 + f1 * df2)
            }
            DerivativeOps::QuotRule => {
                let f1 = self.func1.eval(limit_point);
                let f2 = self.func2.eval(limit_point);
                let df1 = derivative(self.func1, limit_point)?;
                let df2 = derivative(self.func2, limit_point)?;
                let numerator = f2 * df1 - f1 * df2;
                let denominator = f2 * f2;
                Ok(numerator / denominator)
            }
            _ => {
                let dfunc1 = derivative(self.func1, limit_point)?;
                let dfunc2 = derivative(self.func2, limit_point)?;
                Ok(self.scalar_op(dfunc1, dfunc2))
            }
        }
    }

    fn scalar_op(&self, y1: f64, y2: f64) -> f64 {
        match self.op {
            DerivativeOps::Add => y1 + y2,
            DerivativeOps::Sub => y1 - y2,
            DerivativeOps::Mul => y1 * y2,
            DerivativeOps::Div => y1 / y2,
            DerivativeOps::Lin => self.alpha * (y1 + y2),
            DerivativeOps::Pow => y2,
            DerivativeOps::Chain => y2,
            DerivativeOps::ProdRule => y1 * y2,
            DerivativeOps::QuotRule => y1 / y2,
        }
    }
}

impl<'a> EvalScalar for OpOnDerivativeScalarFns<'a> {
    fn eval(&self, x: f64) -> f64 {
        let y1 = self.func1.eval(x);
        let y2 = match self.op {
            DerivativeOps::Chain => self.func2.eval(y1),
            _ => self.func2.eval(x),
        };
        self.scalar_op(y1, y2)
    }
    fn limit(&self, limit_point: f64) -> Result<f64, LimitError> {
        let outer = self.outer_limit(limit_point)?;
        let inner = self.inner_limit(limit_point)?;
        if (outer - inner).abs() < 1e-5 {
            Ok(outer)
        } else {
            Err(LimitError::DifferentLimits)
        }
    }
    fn get_fn(&self) -> Option<fn(f64, &Option<Vec<f64>>) -> f64> {
        None
    }
    fn get_params(&self) -> Option<Vec<f64>> {
        None
    }
}

#[derive(Clone, Copy, Debug)]
pub enum DerivativeOps {
    Add,
    Sub,
    Mul,
    Div,
    Lin,
    Pow,
    Chain,
    ProdRule,
    QuotRule,
}

pub fn validate_derivative_prop(
    limit_point: f64,
    func1: &ScalarFn,
    func2: &ScalarFn,
    prop: DerivativeOps,
    alpha: Option<f64>,
) -> Result<f64, LimitError> {
    let operator = OpOnDerivativeScalarFns::new(func1, func2, prop, alpha);
    operator.limit(limit_point)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::math::{line, log, logpow, poly};

    #[test]
    fn test_derivatives_log_over_line() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = compare_derivatives(limit_point, &log, &line);
        match result {
            Ok(x) => match 0.0f64 <= x && x < 1.0f64 {
                true => assert!(true),
                false => {
                    println!(
                        "x is not in (0, 1): x>0 ? {:?}, x<1 ? {:?}, x = {:?}",
                        0.0f64 < x,
                        x < 1.0f64,
                        x
                    );
                    assert!(false)
                }
            },
            Err(x) => {
                println!("Comparing derivatives failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_derivatives_line_over_log() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = compare_derivatives(limit_point, &line, &log);
        match result {
            Ok(x) => assert!(x > 1.0f64),
            Err(x) => {
                println!("Comparing derivatives failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_derivatives_line_over_line() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let result = compare_derivatives(limit_point, &line, &line);
        match result {
            Ok(x) => assert!((x - 1.0f64).abs() <= 1.0e-6),
            Err(x) => {
                println!("Comparing derivatives failed with: {:?}", x);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_sub_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_derivative_prop(limit_point, &line, &log, DerivativeOps::Sub, None);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op = OpOnDerivativeScalarFns::new(&line, &log, DerivativeOps::Sub, None);
                println!("Subtracting derivatives validation failed with: {:?}", x);
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                println!("Line derivative: {:?}", derivative(&line, limit_point));
                println!("Log derivative: {:?}", derivative(&log, limit_point));
                assert!(false);
            }
        }
    }

    #[test]
    fn test_add_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_derivative_prop(limit_point, &line, &log, DerivativeOps::Add, None);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op = OpOnDerivativeScalarFns::new(&line, &log, DerivativeOps::Add, None);
                println!("Adding derivatives validation failed with: {:?}", x);
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                println!("Line derivative: {:?}", derivative(&line, limit_point));
                println!("Log derivative: {:?}", derivative(&log, limit_point));
                assert!(false);
            }
        }
    }

    #[test]
    fn test_mult_limit_prop_line_log_is_invalid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_derivative_prop(limit_point, &line, &log, DerivativeOps::Mul, None);
        match result {
            Err(_) => assert!(true),
            Ok(_) => {
                println!(
                    "Multiplying derivatives invalidation failed at: {:?}",
                    limit_point
                );
                assert!(false);
            }
        }
    }

    #[test]
    fn test_div_limit_prop_line_log_is_invalid() {
        let limit_point = 5.0f64;
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_derivative_prop(limit_point, &line, &log, DerivativeOps::Div, None);
        match result {
            Err(_) => assert!(true),
            Ok(_) => {
                println!(
                    "Divide derivatives invalidation failed at: {:?}",
                    limit_point
                );
                assert!(false);
            }
        }
    }

    #[test]
    fn test_linearity_limit_prop_line_log_is_valid() {
        let limit_point = 5.0f64;
        let alpha = Some(5.0f64);
        let line = ScalarFn::new(line, None);
        let log = ScalarFn::new(log, None);
        let result = validate_derivative_prop(limit_point, &line, &log, DerivativeOps::Lin, alpha);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op = OpOnDerivativeScalarFns::new(&line, &log, DerivativeOps::Lin, alpha);
                println!("Linearity of derivatives validation failed with: {:?}", x);
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                println!("Line derivative: {:?}", derivative(&line, limit_point));
                println!("Log derivative: {:?}", derivative(&log, limit_point));
                assert!(false);
            }
        }
    }

    #[test]
    fn test_power_rule_log_is_valid() {
        let limit_point = 5.0f64;
        let alpha = Some(2.0f64);
        let exp = f64::exp(1.0);
        let params = Some(vec![exp, alpha.unwrap()]);
        let log = ScalarFn::new(log, None);
        let logsq = ScalarFn::new(logpow, params);
        let result = validate_derivative_prop(limit_point, &log, &logsq, DerivativeOps::Pow, alpha);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op = OpOnDerivativeScalarFns::new(&log, &logsq, DerivativeOps::Pow, alpha);
                println!("Power rule for derivatives validation failed with: {:?}", x);
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                println!("Log^2 derivative: {:?}", derivative(&logsq, limit_point));
                assert!(false);
            }
        }
    }

    #[test]
    fn test_chain_rule_log_of_5xsq_is_valid() {
        let limit_point = 5.0f64;
        let params = Some(vec![0f64, 0f64, 5f64]);
        let log = ScalarFn::new(log, None);
        let five_x_sq = ScalarFn::new(poly, params);
        let result =
            validate_derivative_prop(limit_point, &five_x_sq, &log, DerivativeOps::Chain, None);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op = OpOnDerivativeScalarFns::new(&five_x_sq, &log, DerivativeOps::Chain, None);
                println!("Chain rule for derivatives validation failed with: {:?}", x);
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                assert!(false);
            }
        }
    }

    #[test]
    fn test_prod_rule_log_times_5xsq_is_valid() {
        let limit_point = 5.0f64;
        let params = Some(vec![0f64, 0f64, 5f64]);
        let log = ScalarFn::new(log, None);
        let five_x_sq = ScalarFn::new(poly, params);
        let result =
            validate_derivative_prop(limit_point, &five_x_sq, &log, DerivativeOps::ProdRule, None);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op =
                    OpOnDerivativeScalarFns::new(&five_x_sq, &log, DerivativeOps::ProdRule, None);
                println!(
                    "Product rule for derivatives validation failed with: {:?}",
                    x
                );
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                assert!(false);
            }
        }
    }

    #[test]
    fn test_quot_rule_log_over_5xsq_is_valid() {
        let limit_point = 5.0f64;
        let params = Some(vec![0f64, 0f64, 5f64]);
        let log = ScalarFn::new(log, None);
        let five_x_sq = ScalarFn::new(poly, params);
        let result =
            validate_derivative_prop(limit_point, &five_x_sq, &log, DerivativeOps::QuotRule, None);
        match result {
            Ok(_) => assert!(true),
            Err(x) => {
                let op =
                    OpOnDerivativeScalarFns::new(&five_x_sq, &log, DerivativeOps::QuotRule, None);
                println!(
                    "Quotient rule for derivatives validation failed with: {:?}",
                    x
                );
                println!(
                    "Outer: {:?}, Inner: {:?}",
                    op.outer_limit(limit_point),
                    op.inner_limit(limit_point)
                );
                assert!(false);
            }
        }
    }
}
